/* @flow */

import {
	warn,
	remove,
	isObject,
	parsePath,
	_Set as Set,
	handleError,
	invokeWithErrorHandling,
	noop,
} from "../util/index";

import { traverse } from "./traverse";
import { queueWatcher } from "./scheduler";
import Dep, { pushTarget, popTarget } from "./dep";

import type { SimpleSet } from "../util/index";

let uid = 0;

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
	vm: Component;
	expression: string;
	cb: Function;
	id: number;
	deep: boolean;
	user: boolean;
	lazy: boolean;
	sync: boolean;
	dirty: boolean;
	active: boolean;
	deps: Array<Dep>;
	newDeps: Array<Dep>;
	depIds: SimpleSet;
	newDepIds: SimpleSet;
	before: ?Function;
	getter: Function;
	value: any;

	constructor(
		vm: Component,
		expOrFn: string | Function,
		cb: Function,
		options?: ?Object,
		isRenderWatcher?: boolean
	) {
		this.vm = vm;
		if (isRenderWatcher) {
			vm._watcher = this;
		}
		vm._watchers.push(this);
		// options
		if (options) {
			this.deep = !!options.deep;
			this.user = !!options.user;
			this.lazy = !!options.lazy;
			this.sync = !!options.sync;
			this.before = options.before;
		} else {
			this.deep = this.user = this.lazy = this.sync = false;
		}
		this.cb = cb;
		this.id = ++uid; // uid for batching
		this.active = true;
		this.dirty = this.lazy; // for lazy watchers
		this.deps = [];
		this.newDeps = [];
		this.depIds = new Set();
		this.newDepIds = new Set();
		this.expression =
			process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
		// parse expression for getter
		if (typeof expOrFn === "function") {
			this.getter = expOrFn;
		} else {
			this.getter = parsePath(expOrFn);
			if (!this.getter) {
				this.getter = noop;
				process.env.NODE_ENV !== "production" &&
					warn(
						`Failed watching path: "${expOrFn}" ` +
							"Watcher only accepts simple dot-delimited paths. " +
							"For full control, use a function instead.",
						vm
					);
			}
		}
		this.value = this.lazy ? undefined : this.get();
	}

	/**
	 * Evaluate the getter, and re-collect dependencies.
	 */
	get() {
		pushTarget(this);
		let value;
		const vm = this.vm;
		try {
			value = this.getter.call(vm, vm);
		} catch (e) {
			if (this.user) {
				handleError(e, vm, `getter for watcher "${this.expression}"`);
			} else {
				throw e;
			}
		} finally {
			// "touch" every property so they are all tracked as
			// dependencies for deep watching
			if (this.deep) {
				traverse(value);
			}
			popTarget();
			this.cleanupDeps();
		}
		return value;
	}

	/**
	 * Add a dependency to this directive.
	 */
	addDep(dep: Dep) {
		const id = dep.id;
		if (!this.newDepIds.has(id)) {
			this.newDepIds.add(id);
			this.newDeps.push(dep);
			if (!this.depIds.has(id)) {
				dep.addSub(this);
			}
		}
	}

	/**
	 * Clean up for dependency collection.
	 */
	cleanupDeps() {
		let i = this.deps.length;
		while (i--) {
			const dep = this.deps[i];
			if (!this.newDepIds.has(dep.id)) {
				dep.removeSub(this);
			}
		}
		let tmp = this.depIds;
		this.depIds = this.newDepIds;
		this.newDepIds = tmp;
		this.newDepIds.clear();
		tmp = this.deps;
		this.deps = this.newDeps;
		this.newDeps = tmp;
		this.newDeps.length = 0;
	}

	/**
	 * Subscriber interface.
	 * Will be called when a dependency changes.
	 */
	update() {
		/* istanbul ignore else */
		if (this.lazy) {
			this.dirty = true;
		} else if (this.sync) {
			// 同步更新视图
			this.run();
		} else {
			// 异步更新视图
			queueWatcher(this);
		}
	}

	/**
	 * Scheduler job interface.
	 * Will be called by the scheduler.
	 * 同步更新
	 */
	run() {
		if (this.active) {
			const value = this.get();
			if (
				value !== this.value ||
				// Deep watchers and watchers on Object/Arrays should fire even
				// when the value is the same, because the value may
				// have mutated.
				isObject(value) ||
				this.deep
			) {
				// set new value
				const oldValue = this.value;
				this.value = value;
				if (this.user) {
					const info = `callback for watcher "${this.expression}"`;
					invokeWithErrorHandling(
						this.cb,
						this.vm,
						[value, oldValue],
						this.vm,
						info
					);
				} else {
					this.cb.call(this.vm, value, oldValue);
				}
			}
		}
	}

	/**
	 * Evaluate the value of the watcher.
	 * This only gets called for lazy watchers.
	 */
	evaluate() {
		this.value = this.get();
		this.dirty = false;
	}

	/**
	 * Depend on all deps collected by this watcher.
	 */
	depend() {
		let i = this.deps.length;
		while (i--) {
			this.deps[i].depend();
		}
	}

	/**
	 * Remove self from all dependencies' subscriber list.
	 */
	teardown() {
		if (this.active) {
			// remove self from vm's watcher list
			// this is a somewhat expensive operation so we skip it
			// if the vm is being destroyed.
			if (!this.vm._isBeingDestroyed) {
				remove(this.vm._watchers, this);
			}
			let i = this.deps.length;
			while (i--) {
				this.deps[i].removeSub(this);
			}
			this.active = false;
		}
	}
}
